{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Context Managers in Python"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You should be familiar with `try` and `finally`.\n",
    "\n",
    "We use the `finally` block to make sure a piece of code is executed, whether an exception has happened or not:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "finally ran!\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    10 / 2\n",
    "except ZeroDivisionError:\n",
    "    print('Zero division exception occurred')\n",
    "finally:\n",
    "    print('finally ran!')    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Zero division exception occurred\n",
      "finally ran!\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    1 / 0\n",
    "except ZeroDivisionError:\n",
    "    print('Zero division exception occurred')\n",
    "finally:\n",
    "    print('finally ran!')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You'll see that in both instances, the `finally` block was executed. Even if an exception is raised in the `except` block, the `finally` block will **still** execute!\n",
    "\n",
    "Even if the finally is in a function and there is a return statement in the `try` or `except` blocks:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def my_func():\n",
    "    try:\n",
    "        1/0\n",
    "    except:\n",
    "        return\n",
    "    finally:\n",
    "        print('finally running...')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "finally running...\n"
     ]
    }
   ],
   "source": [
    "my_func()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This is very handy to release resources even in cases where an exception occurs. For example making sure a file is closed after being opened:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "an exception occurred...\n",
      "Closing file...\n"
     ]
    }
   ],
   "source": [
    "try:\n",
    "    f = open('test.txt', 'w')\n",
    "    a = 1 / 0\n",
    "except:\n",
    "    print('an exception occurred...')\n",
    "finally:\n",
    "    print('Closing file...')\n",
    "    f.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We should **always** do that when dealing with files.\n",
    "\n",
    "But that can get cumbersome...\n",
    "\n",
    "So, there is a better way."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's talk about context managers, and the pattern we are trying to solve:\n",
    "\n",
    "1. Run some code to create some object(s)\n",
    "2. Work with object(s)\n",
    "3. Run some code when done to clean up object(s)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Context managers do precisely that.\n",
    "\n",
    "We use a context manager to create and clean up some objects. The key point is that the cleanup needs to happens automatically - we should not have to write code such as the `try...except...finally` code we saw above."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "When we use context managers in conjunction with the `with` statement, we end up with the \"cleanup\" phase happening as soon as the `with` statement finishes:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "inside with: file closed? False\n",
      "after with: file closed? True\n"
     ]
    }
   ],
   "source": [
    "with open('test.txt', 'w') as file:\n",
    "    print('inside with: file closed?', file.closed)\n",
    "print('after with: file closed?', file.closed)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This works even in this case:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def test():\n",
    "    with open('test.txt', 'w') as file:\n",
    "        print('inside with: file closed?', file.closed)\n",
    "        return file"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, we return directly out of the `with` block..."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "inside with: file closed? False\n"
     ]
    }
   ],
   "source": [
    "file = test()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "file.closed"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And yet, the file was still closed.\n",
    "\n",
    "It also works even if we have an exception in the middle of the block:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "inside with: file closed? False\n"
     ]
    },
    {
     "ename": "ValueError",
     "evalue": "",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mValueError\u001b[0m                                Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-10-8ff9d652054f>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m()\u001b[0m\n\u001b[0;32m      1\u001b[0m \u001b[1;32mwith\u001b[0m \u001b[0mopen\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'test.txt'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'w'\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0mf\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      2\u001b[0m     \u001b[0mprint\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'inside with: file closed?'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mf\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mclosed\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 3\u001b[1;33m     \u001b[1;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;31mValueError\u001b[0m: "
     ]
    }
   ],
   "source": [
    "with open('test.txt', 'w') as f:\n",
    "    print('inside with: file closed?', f.closed)\n",
    "    raise ValueError()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "after with: file closed? True\n"
     ]
    }
   ],
   "source": [
    "print('after with: file closed?', f.closed)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Context managers can be used for more than just opening and closing files.\n",
    "\n",
    "If we think about it there are two phases to a context manager:\n",
    "1. when the `with` statement is executing: we **enter** the context\n",
    "2. when the `with` block is done: we **exit** the context\n",
    "\n",
    "We can create our own context manager using a class that implements an `__enter__` method which is executed when we enter the context, and an `__exit__` method that is executed when we exit the context.\n",
    "\n",
    "There is a general pattern that context managers can help us deal with:\n",
    "* Open - Close\n",
    "* Lock - Release\n",
    "* Change - Reset\n",
    "* Enter - Exit\n",
    "* Start - Stop"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The `__enter__` method is quite straightforward. It can (but does not have to) return one or more objects we then use inside the `with` block.\n",
    "\n",
    "The `__exit__` method however is slightly more complicated.\n",
    "\n",
    "1. It needs to return a boolean True/False. This indicates to Python whether to suppress any errors that occurred in the with block. As we saw with files, that was not the case - i.e. it returns a False\n",
    "2. If an error does occur in the with block, the error information is passed to the `__exit__` method - so it needs three things: the exception type, the exception value and the traceback. If no error occured, then those values will simply be None.\n",
    "\n",
    "We haven't covered exceptions in detail yet, so let's quickly see what those three things are:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "ename": "ZeroDivisionError",
     "evalue": "float division by zero",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mZeroDivisionError\u001b[0m                         Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-12-81be666370d0>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m()\u001b[0m\n\u001b[0;32m      2\u001b[0m     \u001b[1;32mreturn\u001b[0m \u001b[1;36m1.0\u001b[0m \u001b[1;33m/\u001b[0m \u001b[1;36m0.0\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      3\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 4\u001b[1;33m \u001b[0mmy_func\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;32m<ipython-input-12-81be666370d0>\u001b[0m in \u001b[0;36mmy_func\u001b[1;34m()\u001b[0m\n\u001b[0;32m      1\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mmy_func\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m     \u001b[1;32mreturn\u001b[0m \u001b[1;36m1.0\u001b[0m \u001b[1;33m/\u001b[0m \u001b[1;36m0.0\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m      3\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m      4\u001b[0m \u001b[0mmy_func\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
      "\u001b[1;31mZeroDivisionError\u001b[0m: float division by zero"
     ]
    }
   ],
   "source": [
    "def my_func():\n",
    "    return 1.0 / 0.0\n",
    "\n",
    "my_func()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The exception type here is `ZeroDivisionError`.\n",
    "\n",
    "The exception value is `float division by zero`.\n",
    "\n",
    "The traceback is an object of type `traceback` (that itself points to other `traceback` objects forming the trace stack) used to generate that text shown in the output.\n",
    "\n",
    "I am not going to cover `traceback` objects at this point - we'll do this in a future part (OOP) of this series."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's go ahead and create a context manager:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class MyContext:\n",
    "    def __init__(self):\n",
    "        self.obj = None\n",
    "        \n",
    "    def __enter__(self):\n",
    "        print('entering context...')\n",
    "        self.obj = 'the Return Object'\n",
    "        return self.obj\n",
    "\n",
    "    def __exit__(self, exc_type, exc_value, exc_traceback):\n",
    "        print('exiting context...')\n",
    "        if exc_type:\n",
    "            print(f'*** Error occurred: {exc_type}, {exc_value}')\n",
    "        return False  # do not suppress exceptions"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can even cause an exception inside the `with` block:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "entering context...\n",
      "exiting context...\n",
      "*** Error occurred: <class 'ValueError'>, \n"
     ]
    },
    {
     "ename": "ValueError",
     "evalue": "",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mValueError\u001b[0m                                Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-14-39a69b57f322>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m()\u001b[0m\n\u001b[0;32m      1\u001b[0m \u001b[1;32mwith\u001b[0m \u001b[0mMyContext\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mas\u001b[0m \u001b[0mobj\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m----> 2\u001b[1;33m     \u001b[1;32mraise\u001b[0m \u001b[0mValueError\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;31mValueError\u001b[0m: "
     ]
    }
   ],
   "source": [
    "with MyContext() as obj:\n",
    "    raise ValueError"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, the `__exit__` method was still called - which is exactly what we wanted in the first place. Also, the exception that was raise inside the `with` block is seen."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can change that by returning `True` from the `__exit__` method:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class MyContext:\n",
    "    def __init__(self):\n",
    "        self.obj = None\n",
    "        \n",
    "    def __enter__(self):\n",
    "        print('entering context...')\n",
    "        self.obj = 'the Return Object'\n",
    "        return self.obj\n",
    "\n",
    "    def __exit__(self, exc_type, exc_value, exc_traceback):\n",
    "        print('exiting context...')\n",
    "        if exc_type:\n",
    "            print(f'*** Error occurred: {exc_type}, {exc_value}')\n",
    "        return True  # suppress exceptions"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "entering context...\n",
      "exiting context...\n",
      "*** Error occurred: <class 'ValueError'>, \n",
      "reached here without an exception...\n"
     ]
    }
   ],
   "source": [
    "with MyContext() as obj:\n",
    "    raise ValueError\n",
    "print('reached here without an exception...')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Look at the output of this code:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "entering context...\n",
      "running inside with block...\n",
      "the Return Object\n",
      "exiting context...\n",
      "the Return Object\n"
     ]
    }
   ],
   "source": [
    "with MyContext() as obj:\n",
    "    print('running inside with block...')\n",
    "    print(obj)\n",
    "print(obj)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice that the `obj` we obtained from the context manager, still exists in our scope after the `with` statement.\n",
    "\n",
    "The `with` statement does **not** have its own local scope - it's not a function!\n",
    "\n",
    "However, the context manager could manipulate the object returned by the context manager:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class Resource:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        self.state = None"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class ResourceManager:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        self.resource = None\n",
    "        \n",
    "    def __enter__(self):\n",
    "        print('entering context')\n",
    "        self.resource = Resource(self.name)\n",
    "        self.resource.state = 'created'\n",
    "        return self.resource\n",
    "    \n",
    "    def __exit__(self, exc_type, exc_value, exc_traceback):\n",
    "        print('exiting context')\n",
    "        self.resource.state = 'destroyed'\n",
    "        if exc_type:\n",
    "            print('error occurred')\n",
    "        return False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "entering context\n",
      "spam = created\n",
      "exiting context\n",
      "spam = destroyed\n"
     ]
    }
   ],
   "source": [
    "with ResourceManager('spam') as res:\n",
    "    print(f'{res.name} = {res.state}')\n",
    "print(f'{res.name} = {res.state}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We still have access to `res`, but it's internal state was changed by the resource manager's `__exit__` method."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Although we already have a context manager for files built-in to Python, let's go ahead and write our own anyways - good practice."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class File:\n",
    "    def __init__(self, name, mode):\n",
    "        self.name = name\n",
    "        self.mode = mode\n",
    "        \n",
    "    def __enter__(self):\n",
    "        print('opening file...')\n",
    "        self.file = open(self.name, self.mode)\n",
    "        return self.file\n",
    "    \n",
    "    def __exit__(self, exc_type, exc_value, exc_traceback):\n",
    "        print('closing file...')\n",
    "        self.file.close()\n",
    "        return False"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "opening file...\n",
      "closing file...\n"
     ]
    }
   ],
   "source": [
    "with File('test.txt', 'w') as f:\n",
    "    f.write('This is a late parrot!')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Even if we have an exception inside the `with` statement, our file will still get closed.\n",
    "\n",
    "Same applies if we return out of the `with` block if we're inside a function:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def test():\n",
    "    with File('test.txt', 'w') as f:\n",
    "        f.write('This is a late parrot')\n",
    "        if True:\n",
    "            return f\n",
    "        print(f.closed)\n",
    "    print(f.closed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "opening file...\n",
      "closing file...\n"
     ]
    }
   ],
   "source": [
    "f = test()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Note that the `__enter__` method can return anything, including the context manager itself.\n",
    "\n",
    "If we wanted to, we could re-write our file context manager this way:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "class File():\n",
    "    def __init__(self, name, mode):\n",
    "        self.name = name\n",
    "        self.mode = mode\n",
    "        \n",
    "    def __enter__(self):\n",
    "        print('opening file...')\n",
    "        self.file = open(self.name, self.mode)\n",
    "        return self\n",
    "    \n",
    "    def __exit__(self, exc_type, exc_value, exc_traceback):\n",
    "        print('closing file...')\n",
    "        self.file.close()\n",
    "        return False"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Of course, now we would have to use the context manager object's `file` property to get a handle to the file:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "opening file...\n",
      "This is a late parrot\n",
      "test.txt\n",
      "r\n",
      "closing file...\n"
     ]
    }
   ],
   "source": [
    "with File('test.txt', 'r') as file_ctx:\n",
    "    print(next(file_ctx.file))\n",
    "    print(file_ctx.name)\n",
    "    print(file_ctx.mode)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
